安安~我是ChiYu~
昨天才把二十道測試題出好,今天輪到我寫第一份答案。我從最單純的搜尋活動開始,打開
頁面,看著那顆藍色的「搜尋活動」按鈕,很順手地寫下:
click_blue_search_button
description: 點擊右側藍色按鈕開始搜尋
有一說一,這個名字很好懂,懂到連自動化測試都知道下一步要按哪裡。問題是,我現在設計
的是給 Agent 使用的 Tool,不是替滑鼠找一份新工作。
這份說明把顏色、位置與點擊方式交代得很完整,卻沒回答更重要的事:它能搜尋什麼、接受
哪些條件、回傳哪些資料,又會不會改變使用者看得見的畫面。按鈕只要換顏色、移到左邊,
或被 Enter 鍵取代,這支 Tool 的名字就開始說謊。
使用者真正想做的是「依條件找活動」。所以我沒有繼續替click_blue_search_button 補 schema,而是把它刪掉,從任務本身重新寫一次。

圖 1:左邊的寫法綁住按鈕外觀;右邊的 search_events 固定任務、輸入、結果與副作用邊界。
這張圖比較的是 Tool 描述的層次:左邊記錄「目前這版 UI 剛好怎麼操作」,右邊說明
「使用者要完成的事」。後者能撐過改版,前者會跟著按鈕一起搬家。
search_events 描述搜尋任務,不綁按鈕顏色與位置重新整理後,我先替 search_events 寫下完整的能力邊界:
| 欄位 | 決定 |
|---|---|
| name | search_events |
| 何時使用 | 使用者要依關鍵字、地點、費用或程度尋找公開活動 |
| input | query、location、price、level,全部可省略 |
| result | count 與活動摘要陣列 |
| UI | 更新使用者可見的活動列表 |
| 副作用 | 唯讀;不收藏、不報名 |
| 找不到 | 成功回傳 count: 0, events: [] |
| 服務失敗 | 回傳可分類、可重試的錯誤,不假裝成空結果 |
這時我還沒有決定要用 Declarative 或 Imperative API,因為那是實作方式。現在更急的是把
任務說清楚:搜尋只負責找公開活動,結果要同步回畫面,而且絕對不會順手替使用者收藏或
報名。
正式 description 也不再教 Agent 怎麼點畫面:
依關鍵字、地點、費用與程度搜尋目前公開活動,
並更新使用者可見的活動列表。
未來按鈕就算改名成「探索場次」,這段 description 仍然成立,Tool 名稱也不需要跟著改。
任務名稱穩定下來後,下一個麻煩是參數。假如 schema 只放一個沒有規則的 filters,Agent
當然很自由;server 收到 cityCode、where、autoRegister 時也會自由到不知道該怎麼辦。
這個專案把搜尋輸入固定成四個欄位:
{
"type": "object",
"additionalProperties": false,
"properties": {
"query": {
"type": "string",
"maxLength": 100,
"description": "公開活動標題或摘要中的關鍵字。"
},
"location": {
"type": "string",
"enum": ["taipei", "kaohsiung", "online"]
},
"price": {
"type": "string",
"enum": ["free", "paid"]
},
"level": {
"type": "string",
"enum": ["beginner", "intermediate", "advanced"]
}
}
}
我沒有使用 cityCode=1、feeType=0 這種還得另外翻譯的值。taipei、free、beginner 可以直接從 Prompt 對應,人類看 trace 時也不必先找代碼表。
additionalProperties: false 則是這次很明確的取捨:搜尋只接受這四個欄位,Agent 不能
自己補上 limit、sortBy 或 autoRegister。少一點「你猜我收不收」,後面的驗證會輕鬆
很多。
不過,schema 只是契約,不是安全邊界。
Chrome WebMCP 最佳實務
也建議程式端繼續驗證輸入,因為模型不保證每次都乖乖照著 schema 送資料。
AgentReady Events 的 server 因此會再檢查一次:
query 最多 100 字。location、price、level 必須在允許的 enum 內。400 VALIDATION_ERROR。Tool schema 幫 Agent 組出合理 input;真正決定資料能不能執行的,仍然是 server validation。
input 整理好後,我原本可以只回一句:
{ "message": "搜尋完成" }
這句話沒有錯,只是幾乎沒用。Agent 不知道找到幾場,也拿不到下一步需要的活動 ID。使用者
若接著問「第一場幾點開始」,整段流程只好重新猜一次。
所以 search_events 會回傳 count 與公開活動摘要:
{
"count": 1,
"events": [
{
"id": "evt-webmcp-intro",
"url": "/events/evt-webmcp-intro",
"title": "WebMCP 入門工作坊",
"summary": "從語意 HTML 到第一個網站 Tool。",
"startsAt": "2027-01-23T10:00:00+08:00",
"location": "taipei",
"price": "free",
"level": "beginner"
}
]
}
id 可以交給下一支 Tool 取得詳情,url 則讓 Agent 使用網站提供的 route,不必自己拼
網址。結果只放公開搜尋需要的欄位,Email、內部資料與完整報名物件都不會跟著出門。
這樣一來,昨天題庫中的 ORD-01 才有機會照順序完成:先用 search_events 找到活動,再把
同一個 opaque ID 交給 get_event_details。如果 result 沒有 ID,多步驟任務從第一步就已經
斷線。
搜尋結果是空的,不一定代表真的沒有活動。也可能是上游服務暫時失敗,或 Agent 傳進來的
enum 根本不合法。三種狀態如果都回 events: [],Agent 只會很有禮貌地把系統故障介紹成
「目前沒有符合的活動」。
因此 result contract 把它們分開:
沒有符合活動 → count 0,成功
上游暫時失敗 → TEMPORARY_FAILURE,retryable true
輸入 enum 無效 → INVALID_INPUT,retryable false
第一種可以請使用者調整搜尋條件;第二種可以稍後重試;第三種則應修正 input。錯誤名稱不是
為了讓 JSON 看起來正式,而是要讓 Agent 知道下一步能做什麼。
search_events 契約規格寫完後,我用三個問題回頭檢查:
接著我跑了一輪聚焦測試,鎖定正式名稱、四個搜尋欄位、五 Tool catalog 與 evidence 規則,
共 11 項通過。這份結果只證明 contract 與目前程式一致,證據仍停在 E2 harness;Chrome
是否看見 Tool、Agent 會不會從自然語言選中它,都還得在後面用真實環境回答。
今天,我把畫面上的藍色按鈕整理成一項不依賴外觀的搜尋任務。開頭那支click_blue_search_button 也正式退場,算是替明天省下一點麻煩。
因為活動網站還有詳情、收藏、報名與取消。如果每顆按鈕都照同樣方式包成 Tool,catalog
很快就會變成 UI 元件戶口名簿。明天我會把所有候選能力攤開,判斷哪些值得保留、哪些只該
留在人類介面裡。